package com.hmdp.service.impl;

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.hmdp.dto.Result;
import com.hmdp.entity.Shop;
import com.hmdp.mapper.ShopMapper;
import com.hmdp.service.IShopService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.CacheClient;
import com.hmdp.utils.RedisConstants;
import com.hmdp.utils.RedisData;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * <p>
 * 服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {

    @Autowired
    private ShopMapper shopMapper;
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private RedissonClient redissonClient;

    @Autowired
    private ThreadPoolExecutor threadPoolExecutor;

    @Autowired
    private CacheClient cacheClient;

    @Override
    public Result queryShopById(Long id) {
        // 1.使用bitmap和bloomfilter解决缓存穿透问题
        //Shop shop = cachePenetration(id);

        // 2.使用分布式锁解决缓存击穿问题
       Shop shop = cacheBreakDownWithLock(id);

        // 3.使用逻辑过期时间解决缓存击穿问题
        //Shop shop = cacheBreakDownWithLogicExpireTime(id);

        // 4.使用CacheClient工具类---解决缓存穿透问题(不存在的key)---根据指定的key查询缓存，并反序列化为指定类型，利用缓存空值的方式解决
        // Shop shop = cacheClient.queryWithPassThrough(RedisConstants.CACHE_SHOP_KEY, id, Shop.class,(shopId) -> getById(shopId), RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);

        // 5.缓存击穿问题---加分布式锁 (热点Key问题)
        //Shop shop = cacheClient.queryWithMutex(RedisConstants.CACHE_SHOP_KEY, id, Shop.class, (shopId) -> getById(shopId), RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);

        // 6.缓存击穿问题---使用逻辑过期时间解决 (热点Key问题)
        //Shop shop = cacheClient.queryWithLogicExpire(RedisConstants.CACHE_SHOP_KEY, id, Shop.class, this::getById, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
        return Result.ok(shop);
    }

    @Override
    public Result safeUpdateById(Shop shop) {
        Long id = shop.getId();
        String cacheKey = RedisConstants.CACHE_SHOP_KEY + id;
        if (id == null){
            return Result.fail("店铺id不能为空");
        }
        updateById(shop);
        stringRedisTemplate.delete(cacheKey);
        return Result.ok();
    }


    // 缓存击穿问题---使用逻辑过期时间解决 (热点Key问题)
    public Shop cacheBreakDownWithLogicExpireTime(Long id) {
        // 1.从redis查询商铺缓存
        String cacheKey = RedisConstants.CACHE_SHOP_KEY + id;
        String data = stringRedisTemplate.opsForValue().get(cacheKey);
        // 2.判断是否存在
        if (StrUtil.isBlank(data)) {
            return null;
        }
        // 3.命中，需要先把json反序列化为对象
        RedisData redisData = JSONUtil.toBean(data, RedisData.class);
        Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
        // 4.判断是否过期
        if (redisData.getExpireTime().isAfter(LocalDateTime.now())) {
            // 4.1未过期,直接返回
            return shop;
        }
        // 4.2.已过期，需要缓存重建
        // 5.缓存重建
        // 5.1.获取互斥锁
        Boolean lockFlag = tryLock(id);
        // 5.2.判断是否获取锁成功
        if (BooleanUtil.isTrue(lockFlag)) {
            // 5.3.双重检查：再次确认缓存是否仍需要重建（在获取锁后立即检查）
            try {
                String checkData = stringRedisTemplate.opsForValue().get(cacheKey);
                if (StrUtil.isNotBlank(checkData)) {
                    RedisData checkRedisData = JSONUtil.toBean(checkData, RedisData.class);
                    if (checkRedisData.getExpireTime().isAfter(LocalDateTime.now())) {
                        // 其他线程已经重建了缓存且未过期，直接返回
                        return JSONUtil.toBean((JSONObject) checkRedisData.getData(), Shop.class);
                    }
                    // 确认确实已过期才重建
                    if (checkRedisData.getExpireTime().isBefore(LocalDateTime.now())) {
                        // 异步重建缓存
                        threadPoolExecutor.submit(() -> {
                            try {
                                saveShopRedis(id, 180L);
                            } catch (Exception e) {
                                throw new RuntimeException(e);
                            }
                        });
                    }
                }
            } finally {
                unLock(id);
            }
        }

        // 6.返回过期的商铺信息
        return shop;
    }


    // 4.缓存击穿问题---加分布式锁 (热点Key问题)
    public Shop cacheBreakDownWithLock(Long id) {
        // 缓存击穿
        String cacheKey = RedisConstants.CACHE_SHOP_KEY + id;
        // 1.查询缓存
        String shopValue = stringRedisTemplate.opsForValue().get(cacheKey);
        if (StrUtil.isNotBlank(shopValue)) {
            // 2.存在,直接返回
            Shop shop = JSONUtil.toBean(shopValue, Shop.class);
            return shop;
        }
        Boolean lock = tryLock(id);
        Shop shop = null;
        try {
            if (!lock) {
                try {
                    TimeUnit.MILLISECONDS.sleep(50);// 休眠50毫秒
                    return cacheBreakDownWithLock(id); // 递归调用,尝试获取锁
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            // 3.1双重检查缓存，防止重复重建
            String shopValueAfterLock = stringRedisTemplate.opsForValue().get(cacheKey);
            if (StrUtil.isNotBlank(shopValueAfterLock)) {
                return JSONUtil.toBean(shopValueAfterLock, Shop.class);
            }

            // 3.2不存在,根据id查询数据库,并存入缓存
            shop = shopMapper.selectById(id);
            // 4.数据库不存在,返回错误
            if (shop == null) {
                stringRedisTemplate.opsForValue().set(cacheKey, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
                return null;
            }
            String strShop = JSONUtil.toJsonStr(shop);
            //1. 商铺数据缓存未设置过期时间 --- 存在数据不一致问题
            //2. 商铺数据缓存未对空对象进行保护 --- 存在缓存穿透问题
            stringRedisTemplate.opsForValue().set(cacheKey, strShop, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
        } catch (RuntimeException e) {
            throw new RuntimeException(e);
        } finally {
            unLock(id);
        }
        return shop;
    }


    // 缓存穿透cachePenetration(即使用不存在的id，直接访问数据库)
    public Shop cachePenetration(Long id) {
        // 缓存穿透
        // 判断店铺是否存在，避免缓存穿透cachePenetration(即使用不存在的id，直接访问数据库)
        RBloomFilter<Object> bloomFilter = redissonClient.getBloomFilter(RedisConstants.BLOOMFITER_KEY);
        if (!bloomFilter.contains(id)) {
            return null;
        }
        System.out.println("布隆过滤器是否有商铺id：" + bloomFilter.contains(id));
        Boolean bit = redisTemplate.opsForValue().getBit(RedisConstants.BITMAP_KEY, id);
        if (!bit) {
            return null;
        }
        System.out.println("布隆过滤器是否有商铺id：" + bit);
        String cacheKey = RedisConstants.CACHE_SHOP_KEY + id;
        // 1.查询缓存
        String shopValue = stringRedisTemplate.opsForValue().get(cacheKey);
        if (StrUtil.isNotBlank(shopValue)) {
            // 2.存在,直接返回
            Shop shop = JSONUtil.toBean(shopValue, Shop.class);
            return shop;
        }
        // 3.不存在,根据id查询数据库,并存入缓存
        Shop shop = shopMapper.selectById(id);
        // 4.数据库不存在,返回错误
        if (shop == null) {
            stringRedisTemplate.opsForValue().set(cacheKey, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
            return null;
        }
        String strShop = JSONUtil.toJsonStr(shop);
        //1. 商铺数据缓存未设置过期时间 --- 存在数据不一致问题
        //2. 商铺数据缓存未对空对象进行保护 --- 存在缓存穿透问题
        stringRedisTemplate.opsForValue().set(cacheKey, strShop, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
        return shop;
    }


    // 模拟缓存预热，并加入逻辑过期时间
    public void saveShopRedis(Long id, Long expireSeconds) {
        Shop shop = getById(id);
        RedisData redisData = new RedisData();
        redisData.setData(shop);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
        stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));

    }


    public Boolean tryLock(Long id) {
        // 获取分布式锁，并设置分布式锁过期时间，防止发生异常无法释放锁
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(RedisConstants.LOCK_SHOP_KEY + id, "1", RedisConstants.LOCK_SHOP_TTL, TimeUnit.SECONDS);
        // 判断获取锁成功 不能直接返回flag,容器自动拆箱的时候可能会报空指针异常
        return BooleanUtil.isTrue(flag);
    }

    public void unLock(Long id) {
        stringRedisTemplate.delete(RedisConstants.LOCK_SHOP_KEY + id);
    }
}
